1 /*
2 * Katie James
3 * 2048
4 * CSIS 304 JavaScript Project
5 */
6
7 //Using the p5.js third-party JavaScript library
8
9 //constant declarations
10 const GRID_SIZE = 4;
11 const CELL_SIZE = 100;
12
13 //variable declarations
14 var canvas;
15 var grid;
16 var gameOver;
17 var score;
18 var gameWon;
19
20 //DOM
21 scoreContainer = document.getElementById("score");
22
23 /* automatically called by third-party p5 */
24 function setup() {
25 canvas = createCanvas(GRID_SIZE * CELL_SIZE + 50, GRID_SIZE * CELL_SIZE + 50);
26 background(187, 173, 160);
27 centerCanvas(canvas);
28 newGame();
29 noLoop();
30 updateGrid();
31 }
32
33 /* centers the game canvavs on window */
34 function centerCanvas() {
35 var x = (windowWidth - width) / 2;
36 var y = (windowHeight - height) / 2 + 40;
37 canvas.position(x, y);
38 }
39
40 /* recenters the game canvas if the window is resized */
41 function windowResized() {
42 centerCanvas();
43 }
44
45 /* creates a new game with the starting game board */
46 function newGame() {
47 //fill grid with empty values (aka 0)
48 grid = new Array(GRID_SIZE * GRID_SIZE).fill(0);
49 gameOver = false;
50 gameWon = false;
51 score = 0;
52 //add the two starting tiles
53 addRandomTile();
54 addRandomTile();
55 }
56
57 /* updates the game grid with the correct tiles/tile colors, displays the score, and stops
58 the game if there are no moves left or the 2048 tile is reached */
59 function updateGrid() {
60 displayScore();
61 drawGrid();
62 if (gameOver) {
63 displayGameOver();
64 }
65 else if (gameWon) {
66 displayGameWon();
67 }
68 }
69
70 /* automatically called when a key is pressed. Mmoves the tiles on the grid
71 respective to which arrow key is pressed. If Enter is pressed when the game
72 is over, a new game is started. */
73 function keyPressed() {
74 if (!gameOver && !gameWon) {
75 switch (keyCode) {
76 case UP_ARROW:
77 verticalSlide(keyCode);
78 updateGrid();
79 break;
80 case DOWN_ARROW:
81 verticalSlide(keyCode);
82 updateGrid();
83 break;
84 case RIGHT_ARROW:
85 horizontalSlide(keyCode);
86 updateGrid();
87 break;
88 case LEFT_ARROW:
89 horizontalSlide(keyCode);
90 updateGrid();
91 break;
92 }
93 }
94 //if game won/over and enter key is pressed, refresh page to start new game
95 else if (keyCode === ENTER) {
96 if (gameWon) {
97 location.reload();
98 }
99 else {
100 location.reload();
101 }
102 }
103 }
104
105 /* slides the tiles vertically (up or down) and combines tiles of the
106 same value if they collide. */
107 function verticalSlide(direction) {
108 var previousGrid = [];
109 var column;
110 var filler;
111
112 arrayCopy(grid, previousGrid);
113
114 for (var i = 0; i < GRID_SIZE; i++) {
115 column = [];
116 //get column
117 for (var j = i; j < GRID_SIZE * GRID_SIZE; j += 4) {
118 column.push(grid[j]);
119 }
120
121 //combine like values in given vertical direction
122 column = combine(column, direction);
123
124 //remove all empty values
125 column = column.filter(notEmpty);
126
127 //add the correct number of empty values after the nonempty values
128 filler = new Array(GRID_SIZE - column.length).fill(0);
129 if (direction === UP_ARROW) {
130 column = column.concat(filler);
131 }
132 else {
133 column = filler.concat(column);
134 }
135
136 //update the current column in the grid
137 for (var k = 0; k < column.length; k++) {
138 grid[k * GRID_SIZE + i] = column[k];
139 }
140
141 //combine values that were previously separated, but now adajacent
142 combine(column, direction);
143 }
144 checkSlide(previousGrid);
145 }
146
147 /* slides the tiles horizontally (left or right) and combines tiles of the
148 same value if they collide. */
149 function horizontalSlide(direction) {
150 var previousGrid = [];
151 var row;
152 var filler;
153
154 arrayCopy(grid, previousGrid);
155
156 for (var i = 0; i < GRID_SIZE; i++) {
157 //get row
158 row = grid.slice(i * GRID_SIZE, i * GRID_SIZE + GRID_SIZE);
159
160 //combine like values in given horizontal direction
161 row = combine(row, direction);
162
163 //remove all empty values
164 row = row.filter(notEmpty);
165
166 //add the correct number of empty values after the nonempty values
167 filler = new Array(GRID_SIZE - row.length).fill(0);
168 if (direction === LEFT_ARROW) {
169 row = row.concat(filler);
170 }
171 else {
172 row = filler.concat(row);
173 }
174
175 //remove the current row from the grid and add the updated row
176 grid.splice(i * GRID_SIZE, GRID_SIZE);
177 grid.splice(i * GRID_SIZE, 0, ...row);
178 }
179 checkSlide(previousGrid);
180 }
181
182 /* checks that a title is nonempty (value is not zero). Used to filter a row of tiles */
183 function notEmpty(x) {
184 return x > 0;
185 }
186
187 /* checks, after a key is pressed, if anything on the grid actually moved or if the
188 game is over */
189 function checkSlide(previousGrid) {
190 if (!(grid.every((x, i) => x === previousGrid[i]))) {
191 addRandomTile();
192 }
193 if (!movesLeft()) {
194 gameOver = true;
195 }
196 }
197
198 /* checks if there are any moves to play. In other words, there is either an empty
199 tile or if two adjacent tiles have the same value. */
200 function movesLeft() {
201 var movesLeft = false;
202 var flag = false;
203 var currentTile;
204 var right;
205 var bottom;
206
207 for (var i = 0; i < GRID_SIZE; i++) {
208 for (var j = 0; j < GRID_SIZE; j++) {
209 if (!flag) {
210 currentTile = grid[i * GRID_SIZE + j];
211
212 //if grid still has empty spots, there are moves left
213 if (currentTile === 0) {
214 movesLeft = true;
215 flag = true;
216 }
217 else {
218 if (j < GRID_SIZE - 1) {
219 right = grid[i * GRID_SIZE + j + 1];
220 }
221 else {
222 right = 0;
223 }
224 if (i < GRID_SIZE - 1) {
225 bottom = grid[(i + 1) * GRID_SIZE + j];
226 }
227 else {
228 bottom = 0;
229 }
230 //if a neighbor of the current tile has the same value, there are moves left
231 if (currentTile === right || currentTile === bottom) {
232 movesLeft = true;
233 flag = true;
234 }
235 }
236 }
237 }
238 }
239
240 return movesLeft;
241 }
242
243 /* combines tiles of the same values together of a specific row in a given direction */
244 function combine(row, direction) {
245 switch (direction) {
246 case DOWN_ARROW:
247 row = combineDownRight(row);
248 break;
249 case RIGHT_ARROW:
250 row = combineDownRight(row);
251 break;
252 case UP_ARROW:
253 row = combineUpLeft(row);
254 break;
255 case LEFT_ARROW:
256 row = combineUpLeft(row);
257 break;
258 }
259
260 return row;
261 }
262
263 /* combines tiles of the same value togther downwards or to the right */
264 function combineDownRight(row) {
265 var x;
266 var y;
267
268 for (var i = row.length - 1; i > 0; i--) {
269 //get current and subseqent tiles
270 x = row[i];
271 index = i - 1;
272 y = row[index];
273
274 //skip empty tiles until a nonempty tile or the beginning of the row is reached
275 while (y === 0 && index > 0) {
276 y = row[index--];
277 }
278
279 //if the adjacent tiles have equal value, combine and update score
280 if (x === y && x !== 0) {
281 row[i] = x + y;
282 score += row[i];
283 row[index] = 0;
284 if (row[i] === 2048) {
285 gameWon = true;
286 }
287 }
288 }
289
290 return row;
291 }
292
293 /* combines tiles of the same value together upwards or to the left */
294 function combineUpLeft(row) {
295 var x;
296 var y;
297
298 for (var i = 0; i < row.length - 1; i++) {
299 //get current and subsequent tiles
300 x = row[i];
301 index = i + 1;
302 y = row[index];
303
304 //skip empty tiles until a nonempty tile or the end of the row is reached
305 while (y === 0 && index < row.length - 1) {
306 y = row[index++];
307 }
308
309 //if the adjacent tiles have equal value, combine and update score
310 if (x === y && x !== 0) {
311 row[i] = x + y;
312 score += row[i];
313 row[index] = 0;
314 if (row[i] === 2048) {
315 gameWon = true;
316 }
317 }
318 }
319
320 return row;
321 }
322
323 /* adds a 2 or 4 tile to an empty spot in the grid randomly */
324 function addRandomTile() {
325 var emptyTiles = [];
326 var index;
327 var newTile = [2, 4];
328
329 //add the indices of all empty tiles to the emptyTiles array
330 grid.forEach(function(value, index) {
331 if (value === 0) {
332 emptyTiles.push(index);
333 }
334 });
335
336 if (emptyTiles.length > 0) {
337 //get the index of a random empty tile in the grid
338 index = emptyTiles[Math.floor(Math.random() * emptyTiles.length)];
339 //set the value of that empty tile to 2 or 4, randomly chosen
340 grid[index] = newTile[Math.floor(Math.random() * newTile.length)];
341 }
342 }
343
344 /*----------------STYLE----------------*/
345 /* insert the score in the score container */
346 function displayScore() {
347 scoreContainer.innerHTML = `${score}`;
348 }
349
350 /* displays the Game Over message */
351 function displayGameOver() {
352 displayText("Game Over!\nHit Enter to Play Again", color(119, 110, 101), 32, width / 2, height / 2);
353 }
354
355 /* displays the Game Won message */
356 function displayGameWon() {
357 displayText("You Win!\nHit Enter to Play Again", color(119, 110, 101), 32, width / 2, height / 2);
358 }
359
360 /* general function to display text on the canvas */
361 function displayText(message, color, size, xpos, ypos) {
362 textAlign(CENTER);
363 textSize(size);
364 fill(color);
365 text(message, xpos, ypos);
366 }
367
368 /* draw and style the game grid */
369 function drawGrid() {
370 var c;
371
372 for (var i = 0; i < GRID_SIZE; i++) {
373 for (var j = 0; j < GRID_SIZE; j++) {
374 //color of tile depends on value of tile
375 switch (grid[i * GRID_SIZE + j]) {
376 case 0:
377 c = color("#CDC0B4");
378 break;
379 case 2:
380 c = color("#EEE4DA");
381 break;
382 case 4:
383 c = color("#EDE0C8");
384 break;
385 case 8:
386 c = color("#B2DFDB");
387 break;
388 case 16:
389 c = color("#80CBC4");
390 break;
391 case 32:
392 c = color("#4DB6AC");
393 break;
394 case 64:
395 c = color("#26A69A");
396 break;
397 case 128:
398 c = color("#009688");
399 break;
400 case 256:
401 c = color("#00897B");
402 break;
403 case 512:
404 c = color("#00796B");
405 break;
406 case 1024:
407 c = color("#00695C");
408 break;
409 case 2048:
410 c = color("#004D40");
411 break;
412 }
413
414 //fill tile
415 fill(c);
416 //thickness of tile border
417 strokeWeight(2);
418 //color of tile border
419 stroke(c);
420 //draw rectangle with rounded edges for each tile
421 rect(j * CELL_SIZE + j * 10 + 10, i * CELL_SIZE + i * 10 + 10, CELL_SIZE, CELL_SIZE, 5);
422
423 if (grid[i * GRID_SIZE + j] !== 0) {
424 displayText(`${grid[i * GRID_SIZE + j]}`,
425 color(255, 255, 255),
426 45,
427 j * CELL_SIZE + j * 10 + 10 + CELL_SIZE / 2,
428 i * CELL_SIZE + i * 10 + 20 + CELL_SIZE / 2);
429 }
430 }
431 }
432 }